﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using COFE.Core.Utils;
using System.Xml.Linq;
using System.IO;
using NUnit.Framework;

namespace COFE.Web.Json
{
    /// <summary>
    /// Convert between Json to Xml format.
    /// </summary>
    public static class JsonSerializationUtils
    {

        #region Original code (which is not working)
        //public static string ToJson(string indent, XElement rootElement)
        //{
        //    Regex matchNumber = new Regex(RegexPatterns.NumberPattern);
        //    List<XObject> propertyList = new List<XObject>();

        //    Func<string, string, bool, string> appendNode = (key, value, lastNode) =>
        //    {
        //        string comma = lastNode ? "" : ",";
        //        if (matchNumber.Match(value).Success)
        //            return String.Format("{0}\"{1}\" : {2}{3}", indent + spacing, key, value, comma);
        //        return String.Format("{0}\"{1}\" : \"{2}\"{3}", indent + spacing, key, value, comma);
        //    };
        //    Func<XAttribute, bool, string> appendAttribute = (attrib, lastNode) =>
        //    {
        //        return appendNode(attrib.Name.LocalName, attrib.Value, lastNode);
        //    };
        //    Func<XElement, bool, string> appendElement =
        //        (element, lastNode) =>
        //        {
        //            string retVal = "";

        //            if (element.Elements().Count() > 0) // Has sub elements
        //            {
        //                retVal += String.Format("{0}\"{1}\" : [", indent + spacing, element.Name.LocalName) + Environment.NewLine;
        //                foreach (var ele in element.Elements())
        //                {
        //                    retVal += ToJson(indent + spacing, ele);
        //                    if (!ele.Equals(element.Elements().Last()))
        //                        retVal += ",";
        //                    retVal += Environment.NewLine;
        //                }
        //                retVal += indent + spacing + "]" + Environment.NewLine;
        //            }
        //            else
        //            {
        //                if (!String.IsNullOrEmpty(element.Value))
        //                    retVal += appendNode(element.Name.LocalName, element.Value, lastNode);
        //                else retVal += ToJson(indent + spacing, element);
        //            }

        //            return retVal;
        //        };
        //    Func<XObject[], string> appendPropertyList = (propList) =>
        //        {
        //            if (propList.Length == 0)
        //                return "";

        //            StringBuilder sb = new StringBuilder();
        //            sb.AppendLine(indent + "{");
        //            for (int i = 0; i < propList.Length; i++)
        //            {
        //                XObject xobj = propList[i];

        //                if (xobj is XAttribute)
        //                    sb.AppendLine(appendAttribute(xobj as XAttribute, i == propList.Length -1));
        //                else if (xobj is XElement)
        //                    sb.AppendLine(appendElement(xobj as XElement, i == propList.Length - 1));
        //                else throw new ArgumentException();
        //            }
        //            sb.Append(indent + "}");
        //            return sb.ToString();
        //        };

        //    foreach (var attribute in rootElement.Attributes())
        //        propertyList.Add(attribute);
        //    foreach (var element in rootElement.Elements())
        //        propertyList.Add(element);
        //    return (propertyList.Count > 0) ? appendPropertyList(propertyList.ToArray()) :
        //        appendNode(appendElement.
        //        ;
        //}
        //public static string ToJson(XElement rootElement)
        //{
        //    return ToJson("", rootElement);
        //}
        #endregion

        static string spacing = "   ";
        static Regex matchNumber = new Regex(RegexPatterns.NumberPattern);
        static Regex matchPropertyValue = new Regex(RegexPatterns.jsonPropertyValuePattern);
        static Regex matchList = new Regex(RegexPatterns.jsonListStartPattern);
        static XName[] xnameList = XNames.GetAllXNames();
        static Dictionary<XName, XName> collectionDic = new Dictionary<XName, XName>()
        {
            { XNames.Links, XNames.Link },
            { XNames.Entries, XNames.Entry },
            { XNames.Events, XNames.Event }
        };

        static List<XName> attributeList = new List<XName>()
        {
            XNames.StatusType, XNames.ID,
            XNames.EntryBaseType,
            XNames.RelationType, XNames.MediaType, XNames.Uri
        };

        #region ToJsonCode
        private static string getIndent(int level)
        {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < level; i++)
                sb.Append(spacing);
            return sb.ToString();
        }

        private static string escapeJSON(string input)
        {
            input = input.Replace("\\", "\\\\")
                .Replace("\"", "\\\"");
            return input;
        }

        private static string getKeyValueString(int level, string key, string value, bool lastNode)
        {
            string comma = lastNode ? "" : ",";
            if (matchNumber.Match(value).Success)
                return String.Format("{0}\"{1}\" : {2}{3}", getIndent(level + 1), key.ToLower(), escapeJSON(value), comma);
            return String.Format("{0}\"{1}\" : \"{2}\"{3}", getIndent(level + 1), key.ToLower(), escapeJSON(value), comma);
        }

        private static string getAttributeString(int level, XAttribute attrib, bool lastNode)
        {
            return getKeyValueString(level, attrib.Name.LocalName, attrib.Value, lastNode);
        }

        private static string getElementString(int level, XElement element, bool lastNode)
        {
            StringBuilder sb = new StringBuilder();
            //if (collectionDic.Keys.Contains(element.Name)) //Collection, e.g. Links or Files
            if (element.HasAttributes || element.HasElements)
            {
                sb.AppendLine(String.Format("{0}\"{1}\" : [",
                    getIndent(level + 1), element.Name.LocalName.ToLower()));

                foreach (var ele in element.Elements())
                {
                    sb.Append(toJson(level + 1, ele));
                    if (element.HasAttributes || 
                        !ele.Equals(element.Elements().Last()))
                        sb.AppendLine(",");
                }

                foreach (var attr in element.Attributes())
                {
                    sb.Append(
                        getKeyValueString(level + 1,
                        attr.Name.LocalName, attr.Value, true));

                    if (!attr.Equals(element.Attributes().Last()))
                        sb.AppendLine(",");
                }

                sb.Append(String.Format("]", getIndent(level)));
                if (!lastNode)
                    sb.Append(',');
                return sb.ToString();
            }
            else
            {
                return getKeyValueString(level, element.Name.LocalName, element.Value, lastNode);
            }
        }

        private static string toJson(int level, XElement rootElement)
        {
            StringBuilder sb = new StringBuilder();

            if (!rootElement.HasElements && !rootElement.HasAttributes)
                sb.Append(getKeyValueString(level,
                    rootElement.Name.LocalName, rootElement.Value, false).TrimEnd(',',' '));
            else
            {
                sb.AppendLine(getIndent(level) + "{");
                List<XObject> itemList = new List<XObject>();
                itemList.AddRange(from a in rootElement.Attributes() select (XObject)a);
                itemList.AddRange(from e in rootElement.Elements() select (XObject)e);

                foreach (var item in itemList)
                    if (item is XAttribute)
                        sb.AppendLine(getAttributeString(level, item as XAttribute, item.Equals(itemList.Last())));
                    else if (item is XElement)
                        sb.AppendLine(getElementString(level, item as XElement, item.Equals(itemList.Last())));
                sb.Append(getIndent(level) + "}");
            }


            return sb.ToString();
        }
        #endregion

        #region FromJsonCode

        private static XName lookupXName(string elementName)
        {
            foreach (var xname in xnameList)
                if (xname.LocalName.Equals(elementName, StringComparison.InvariantCultureIgnoreCase))
                    return xname;
            return null;
        }

        /// <summary>
        /// Recrusively update XElement from dictionary.
        /// </summary>
        private static void updateProperties(XElement elementToUpdate, Dictionary<string, object> dic)
        {
            foreach (var pp in dic)
            {
                XName key = lookupXName(pp.Key);
                if (key != null)
                {
                    if (pp.Value is List<object>)
                    {
                        var rootElement = new XElement(key);
                        elementToUpdate.Add(rootElement);
                        foreach (var item in (pp.Value as List<object>))
                            if (item is Dictionary<string, object> && collectionDic.ContainsKey(key))
                            {
                                var subElement = new XElement(collectionDic[key]);
                                rootElement.Add(subElement);
                                updateProperties(subElement, item as Dictionary<string, object>);
                            }

                    }
                    else
                        if (pp.Value is Dictionary<string, object>)
                        {
                            var subElement = new XElement(key);
                            elementToUpdate.Add(subElement);
                            updateProperties(subElement, pp.Value as Dictionary<string, object>);
                        }
                        else
                        {
                            if (attributeList.Contains(key))
                                elementToUpdate.Add(new XAttribute(key, pp.Value));
                            else elementToUpdate.Add(new XElement(key, pp.Value));
                        }
                }
                else
                    elementToUpdate.Add(new XElement(pp.Key, pp.Value));
            }
        }
        /// <summary>
        /// Find the appropriate close bracket (supported : ])}>), this is required because of JSON have many levels of bracket.
        /// </summary>        
        private static int findCloseBracket(string jsonText, int startPos, char startBracket)
        {

            char endBracket = ']';
            switch (startBracket)
            {
                case '[': endBracket = ']'; break;
                case '(': endBracket = ')'; break;
                case '{': endBracket = '}'; break;
                case '<': endBracket = '>'; break;
            }

            Assert.AreEqual(jsonText.Count((c) => { return c == startBracket; }),
            jsonText.Count((c) => { return c == endBracket; }));

            int startBracketPos = jsonText.IndexOf(startBracket, startPos);
            if (startBracketPos == -1)
                return -1;

            int workPos = startBracketPos;
            int bracketLevelCount = 1;
            while (bracketLevelCount > 0)
            {
                workPos++;
                if (jsonText[workPos] == startBracket) bracketLevelCount++;
                if (jsonText[workPos] == endBracket) bracketLevelCount--;
            }
            return workPos - startPos + 1;
        }
        private enum tokenType { propertyValue, list, none };
        /// <summary>
        /// Find next property value pair ( "HasSubDir" : "True" ), or next item in a list (e.g. { "MediaType":"...", ... })
        /// </summary>
        private static tokenType findNextToken(string jsonText, ref int idxWorked, out string nextToken)
        {
            Match match;
            string lookupText = jsonText.Substring(idxWorked);

            match = matchList.Match(lookupText);
            if (match.Success)
            {
                int matchLength = findCloseBracket(lookupText, 0, '[');
                if (matchLength == -1)
                    throw new ArgumentException();
                nextToken = lookupText.Substring(0, matchLength);
                idxWorked += matchLength;
                return tokenType.list;
            }

            match = matchPropertyValue.Match(lookupText);
            if (match.Success)
            {
                nextToken = lookupText.Substring(0, match.Length);
                idxWorked += match.Length;
                return tokenType.propertyValue;
            }

            nextToken = null;
            return tokenType.none;
        }
        /// <summary>
        /// Convert json string value into Dictionary
        /// </summary>        
        private static Dictionary<string, object> parseSection(string jsonText, int startIdx)
        {
            startIdx = jsonText.IndexOf('{', startIdx) + 1;
            Assert.AreNotEqual(0, startIdx, "start bracket ({) not found.");

            Dictionary<string, object> retDic = new Dictionary<string, object>();
            int workIdx = startIdx;
            string nextToken;
            tokenType ttype = findNextToken(jsonText, ref workIdx, out nextToken);
            while (ttype != tokenType.none)
            {
                switch (ttype)
                {
                    case tokenType.propertyValue:
                        Match match = matchPropertyValue.Match(nextToken);
                        retDic.Add(match.Groups["key"].Value,
                            match.Groups["value"].Value);
                        break;
                    case tokenType.list:
                        Match match2 = matchList.Match(nextToken);
                        Assert.IsTrue(match2.Success);

                        int listJsonStart = nextToken.IndexOf('[');
                        Assert.AreNotEqual(-1, listJsonStart);
                        int listJsonLength = findCloseBracket(nextToken, listJsonStart, '[');
                        string listJsonText = nextToken.Substring(listJsonStart, listJsonLength);
                        var listItems = new List<object>();

                        int workPos = 0;
                        int matchLength = findCloseBracket(listJsonText, workPos, '{'); ;
                        while (matchLength != -1)
                        {
                            string currentJsonText = listJsonText.Substring(workPos, matchLength);
                            listItems.Add(parseJson(currentJsonText));
                            workPos += matchLength;
                            matchLength = findCloseBracket(listJsonText, workPos, '{');
                        }


                        retDic.Add(match2.Groups["key"].Value, listItems);
                        break;
                    default: throw new NotSupportedException();
                }
                ttype = findNextToken(jsonText, ref workIdx, out nextToken);
            }

            return retDic;
        }

        private static Dictionary<string, object> parseJson(string jsonText)
        {
            return parseSection(jsonText, 0);
        }

        #endregion

        /// <summary>
        /// Converts Json (string) to Xml (XElement).
        /// </summary>
        /// <param name="rootKey"></param>
        /// <param name="jsonText"></param>
        /// <returns></returns>
        public static XElement ToXml(XName rootKey, string jsonText)
        {
            var rootElement = new XElement(rootKey);
            updateProperties(rootElement, parseJson(jsonText));
            return rootElement;
        }

        /// <summary>
        /// Converts Json (string) to Xml stream.
        /// </summary>
        /// <param name="rootKey"></param>
        /// <param name="jsonText"></param>
        /// <returns></returns>
        public static Stream ToXmlStream(XName rootKey, string jsonText)
        {
            var ms = new MemoryStream();
            ToXml(rootKey, jsonText).Save(new StreamWriter(ms));
            ms.Flush();
            ms.Seek(0, SeekOrigin.Begin);
            return ms;
        }

        /// <summary>
        /// Convert Xml (XElement) to Json (string).
        /// </summary>
        /// <param name="rootElement"></param>
        /// <returns></returns>
        public static string ToJson(XElement rootElement)
        {
            return toJson(0, rootElement);
            //"{ \"" + rootElement.Name.LocalName + "\":" +  
            //toJson(0, rootElement) + 
            //"}";
        }

        /// <summary>
        /// Convert Xml (XElement to Json stream.
        /// </summary>
        /// <param name="rootElement"></param>
        /// <returns></returns>
        public static Stream ToJsonStream(XElement rootElement)
        {
            return SerializationUtils.ToMemoryStream(ToJson(rootElement));
        }


    }
}
